﻿using System;
using System.Collections;
using System.Web.UI;
using System.Web.UI.WebControls;

using BizElements.Core;
using BizElements.BusinessLayer;
using BizElements.Web;

namespace BizElements.Web.UI
{
    /// <summary>Component that edits and displays a collection of business objects. Uses BoundDataGrid and BusinessObjectsGridAdapter.</summary>
    public partial class CollectionEditor : UserControl, ICollectionEditor
    {
        #region Members.

        /// <summary>Set to true at the end of Page_Load method when it's been ensured that all required objects have been initialized.</summary>
        bool isLoadComplete;
        /// <summary>Filled in AcceptChanges method.</summary>
        BusinessRuleCollection errors = new BusinessRuleCollection();
        /// <summary>Is set in LoadControlState method.</summary>
        bool wasReadOnly;

        #endregion

        #region Init, Load, PrePrender.

        /// <summary>Register the controls as one whose state must be persisted.</summary>
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            this.Page.RegisterRequiresControlState(this);
        }

        /// <summary>Sets control IDs. Waits until parent page' fires InitComplete event to bind fields, ie. fire <see cref="BindFields"/> event.</summary>
        protected void Page_Init(object sender, EventArgs e)
        {
            // Workaround for grid and popup bug. JavaScrip functions and grid child controle use ID instead of ClientID. This ensure that all HTML element and scripts are unqiue.
            this.Grid.ID = this.ID + "Grid";
            this.uiItemPicker.ID = this.ID + "ItemPicker";  
            this.Page.InitComplete += delegate(object o, EventArgs ea) { InitBindings(); };
        }

        /// <summary>Fires <see cref="BindFields"/> event. Parent control/page should bind fields in event handler.</summary>
        private void InitBindings()
        {
            if (this.BindFields != null)
                BindFields(this, new CommandEventArgs(this.CommandName, this.CommandArgument));
        }

        /// <summary>Binds collection on first load, setups buttons and hotkeys, automatically accepts all changes if in spreadsheet-like mode.</summary>
        protected void Page_Load(object sender, EventArgs e)
        {
            if (this.Collection == null)
                return;

            SetupGridActionsAndHotKeys();
            EnsureAdapter();
            if (!IsPostBack)
            {
                BindCollection();
                SwitchButtons(false);
            }

            if (HasReadOnlyChanged())
                SwitchButtons(false);

            if (this.IsPostBack && !this.wasReadOnly && IsInEditAllMode())
                AcceptChanges();

            this.isLoadComplete = true;
        }

        /// <summary>Disables invalid grid actions and ensures that the collection is bound.</summary>
        protected void Page_PreRender(object sender, EventArgs e)
        {
            if (this.Collection == null)
                return;

            DisableInvalidGridActions();
            EnsureCollectionIsBound();
        }

        #endregion

        #region Control state.

        /// <summary>Saves ReadOnly flag to control state.</summary>
        protected override object SaveControlState()
        {
            object baseClassContribution = base.SaveControlState();
            Pair baseStateAndReadOnly = new Pair(baseClassContribution, this.ReadOnly);
            return baseStateAndReadOnly;
        }

        /// <summary>Loads previous value for ReadOnly flag.</summary>
        protected override void LoadControlState(object savedState)
        {
            Pair baseStateAndReadOnly = savedState as Pair;
            if (baseStateAndReadOnly != null)
            {
                base.LoadControlState(baseStateAndReadOnly.First);
                this.wasReadOnly = (bool)baseStateAndReadOnly.Second;
            }
        }

        #endregion

        #region Setup buttons, actions, hotkeys.

        /// <summary>Sets access mode, edit mode (single record or spreadsheet-like) and buttons.</summary>
        private void SetupGridActionsAndHotKeys()
        {
            if (this.ReadOnly)
            {
                this.uiGrid.DoubleClickEnabled = false;
                this.uiGrid.AcceptEditAction = this.uiGrid.CancelEditAction = this.uiGrid.DoubleClickAction = this.uiGrid.DeleteAction = null;
                this.uiGrid.PreviousItemKeyEnabled = this.uiGrid.NextItemKeyEnabled = this.uiGrid.DefaultActionKeyEnabled = this.uiGrid.DeleteKeyEnabled = false;
                this.uiGrid.MoveLeftKeyEnabled = this.uiGrid.MoveUpKeyEnabled = this.uiGrid.MoveRightKeyEnabled = this.uiGrid.MoveDownKeyEnabled = false;
            }
            else if (this.AlwaysEditBoundFields)
            {
                this.uiGrid.DoubleClickEnabled = false;
                this.uiGrid.AcceptEditAction = this.uiGrid.CancelEditAction = this.uiGrid.DoubleClickAction = this.uiGrid.DeleteAction = null;
                this.uiGrid.PreviousItemKeyEnabled = this.uiGrid.NextItemKeyEnabled = this.uiGrid.DefaultActionKeyEnabled = this.uiGrid.DeleteKeyEnabled = false;
                this.uiGrid.MoveLeftKeyEnabled = this.uiGrid.MoveUpKeyEnabled = this.uiGrid.MoveRightKeyEnabled = this.uiGrid.MoveDownKeyEnabled = true;                
            }
            else
            {
                this.uiGrid.AcceptEditAction = JScript.RenderClick(this.uiAcceptChanges, false);
                this.uiGrid.CancelEditAction = JScript.RenderClick(this.uiRejectChanges, false);
                this.uiGrid.DoubleClickAction = JScript.RenderClick(this.uiEditItem, false);
                this.uiGrid.DoubleClickEnabled = true;
                this.uiGrid.DeleteAction = (this.RemoveItemVisible) ? JScript.RenderClick(this.uiRemoveItem, false) : null;

                this.uiGrid.PreviousItemKeyEnabled = this.uiGrid.NextItemKeyEnabled = this.uiGrid.DefaultActionKeyEnabled = true;
                this.uiGrid.DeleteKeyEnabled = this.RemoveItemVisible;

                this.uiGrid.MoveLeftKeyEnabled = this.uiGrid.MoveUpKeyEnabled = this.uiGrid.MoveRightKeyEnabled = this.uiGrid.MoveDownKeyEnabled = false;
            }
        }

        /// <summary>Switches buttons depending on whether the control on current edit mode.</summary>
        private void SwitchButtons(bool isEditing)
        {
            if (this.ReadOnly)
            {
                this.uiAcceptChanges.Visible = this.uiRejectChanges.Visible = false;
                this.uiItemPicker.Visible = this.uiNewItem.Visible = this.uiEditItem.Visible = this.uiRemoveItem.Visible = false;
                this.uiSaveCollection.Visible = this.uiRefetchCollection.Visible = false;
                this.Grid.DeleteKeyEnabled = false;
            }
            else if (this.AlwaysEditBoundFields)
            {
                this.uiAcceptChanges.Visible = this.uiRejectChanges.Visible = false;
                this.uiItemPicker.Visible = this.ItemPickerVisible;
                this.uiNewItem.Visible = this.NewItemVisible;
                this.uiEditItem.Visible = false;
                this.uiRemoveItem.Visible = this.RemoveItemVisible;
                this.Grid.DeleteKeyEnabled = false;
                this.uiSaveCollection.Visible = this.SaveCollectionVisible;
                this.uiRefetchCollection.Visible = this.RefetchCollectionVisible;
                this.uiOpenDetails.Visible = false;
            }
            else
            {
                this.uiAcceptChanges.Visible = this.uiRejectChanges.Visible = isEditing;
                this.uiItemPicker.Visible = this.ItemPickerVisible && !isEditing;
                this.uiNewItem.Visible = this.NewItemVisible && !isEditing;
                this.uiEditItem.Visible = this.EditItemVisible && !isEditing;
                this.uiRemoveItem.Visible = this.RemoveItemVisible && !isEditing;
                this.Grid.DeleteKeyEnabled = this.RemoveItemVisible && !isEditing;
                this.uiSaveCollection.Visible = this.SaveCollectionVisible && !isEditing;
                this.uiRefetchCollection.Visible = this.RefetchCollectionVisible && !isEditing;
                this.uiOpenDetails.Visible = this.OpenDetailsVisible && !isEditing;
            }
        }

        /// <summary>Disable double-click and delete actions if matching buttons are not visible.</summary>
        private void DisableInvalidGridActions()
        {
            if (!this.uiEditItem.Visible)
                this.Grid.DoubleClickAction = null;
            if (!this.uiRemoveItem.Visible)
                this.Grid.DeleteAction = null;
        }

        #endregion

        #region Events.

        /// <summary>CommandName passed when an event is fired.</summary>
        public string CommandName { get; set; }

        /// <summary>CommandArgument passed when an event is fired.</summary>
        public string CommandArgument { get; set; }

        /// <summary>Occurs when parent component must bind fields to grid..</summary>
        public event CommandEventHandler BindFields;

        /// <summary>Occurs when parent component must ensure that all collection items implement ISelfValidatingObject interface.</summary>
        public event CommandEventHandler ConvertCollectionItemsToBusinessObject;

        /// <summary>Occurs when a new item sould be added via AddItems method.</summary>
        public event CommandEventHandler NewItem;

        /// <summary>Occurs when user selects an existed item via popup button control.</summary>
        public event CommandEventHandler ItemPicked;

        /// <summary>Occurs when user request collection to be persisted.</summary>
        public event CommandEventHandler SaveCollection;

        /// <summary>Occurs when user requests that the collection is refetched, ie. when any unsaved changes should be discarded.</summary>
        public event CommandEventHandler RefetchCollection;

        /// <summary>Occurs when user requests that item details should be displayed in dedicated form.</summary>
        public event CommandEventHandler OpenDetails;

        /// <summary>Occurs when collection has changed, ie. an item had been removed, added, edited and successfully validated.</summary>
        public event CommandEventHandler CollectionChanged;

        /// <summary>Cancelable event which occurs when user has initiated "new item" action. Cancel the event if a new item should be added using custom logic, such as a dedicated form. If not canceled then the new item is added via grid UI.</summary>
        public event CancelCommandEventHandler BeginInserting;

        /// <summary>Cancelable event which occurs when user has initiated "new item" action. Cancel the event if a item editing should be performed using custom logic, such as a dedicated form. If not canceled then the item is edited via grid UI.</summary>
        public event CancelCommandEventHandler BeginEditing;

        #endregion

        #region Properties.

        /// <summary>Collection of bound business objects. Objects must either implement <see cref="BizElements.BusinessLayer.ISelfValidatingObject"/> interface or may be converted to objects that implement it when the <see cref="ConvertCollectionItemsToBusinessObject"/> event is fired.</summary>
        public IList Collection { get; set; }

        /// <summary>Control title.</summary>
        public string Caption
        {
            get { return this.uiHeader.Caption; }
            set { this.uiHeader.Caption = value; }
        }

        ValidationLevel validationLevel = ValidationLevel.AllBusinessRules;

        /// <summary>Validation level used during object validation.</summary>
        public ValidationLevel ValidationLevel
        {
            get { return this.validationLevel; }
            set { this.validationLevel = value; }
        }

        /// <summary>Indicates whether popup buttons control used to select existing records is visible. Search engine builder registration and event handling (ItemSelected) must be implemented in parent control/page.</summary>
        public bool ItemPickerVisible { get; set; }

        bool newItemVisible = true;

        /// <summary>Indicates whether "New item" button is visible. If set to true thern an event handler for NewItem event must be wired.</summary>
        public bool NewItemVisible
        {
            get { return this.newItemVisible; }
            set { this.newItemVisible = value; }
        }

        bool editItemVisible = true;

        /// <summary>Indicates whether "Edit selected item(s)" button is visible.</summary>
        public bool EditItemVisible
        {
            get { return this.editItemVisible; }
            set { this.editItemVisible = value; }
        }

        bool removeItemVisible = true;

        /// <summary>Indicates whether "Remove selected item(s)" button is visible.</summary>
        public bool RemoveItemVisible
        {
            get { return this.removeItemVisible; }
            set { this.removeItemVisible = value; }
        }

        /// <summary>Indicates whether edit templates are used by default for all bound fields resulting in a spreadsheet-like mode.</summary>
        public bool AlwaysEditBoundFields
        {
            get { return this.uiGrid.AlwaysEditBoundFields; }
            set { this.uiGrid.AlwaysEditBoundFields = value; }
        }

        /// <summary>Indicates whether the component is in read-only mode, ie. whether the collection is editable. The value may be changed
        /// in design time (ASPC/ASCX file) or in Init phase. No affect after Init phase because that's when the fields are bound to the underlying grid.</summary>
        public bool ReadOnly { get; set; }

        /// <summary>Indicates whether paging is used.</summary>
        public bool AllowPaging
        {
            get { return this.uiGrid.AllowPaging; }
            set { this.uiGrid.AllowPaging = value; }
        }

        /// <summary>Number of items to display on a single page.</summary>
        public int PageSize
        {
            get { return this.uiGrid.PageSize; }
            set { this.uiGrid.PageSize = value; }
        }

        /// <summary>Indicates whether "Save collection" button is visible. If set to true then an event handler for SaveCollection event must be wired.</summary>
        public bool SaveCollectionVisible { get; set; }

        /// <summary>Indicates whether "Refetch collection" button is visible. If set to true then an event handler for RefetchCollection event must be wired.</summary>
        public bool RefetchCollectionVisible { get; set; }

        /// <summary>Indicates whether "Open details in dedicated form" button is visible. If set to true then an event handler for OpenDetails event must be wired.</summary>
        public bool OpenDetailsVisible { get; set; }

        /// <summary>Indicates whether a confirmation is required before child's detail form is opened. Recommendation: set to true when redirecting to a new page and to false if popup window is used.</summary>
        public bool OpenDetailsConfirmationRequired
        {
            get { return this.uiOpenDetails.ConfirmationRequired; }
            set { this.uiOpenDetails.ConfirmationRequired = value; }
        }

        /// <summary>Indicates whether a confirmation is required before child's custom form is opened. Recommendation: set to true when redirecting to a new page and to false if popup window is used.</summary>
        public bool EditItemConfirmationRequired
        {
            get { return this.uiEditItem.ConfirmationRequired; }
            set { this.uiEditItem.ConfirmationRequired = value; }
        }

        /// <summary>Indicates whether a confirmation is required before child's custom form is opened. Recommendation: set to true when redirecting to a new page and to false if popup window is used.</summary>
        public bool NewItemConfirmationRequired
        {
            get { return this.uiNewItem.ConfirmationRequired; }
            set { this.uiNewItem.ConfirmationRequired = value; }
        }

        #endregion

        #region Components/child controls - grid, item picker.

        /// <summary>Grid to which collection is bound.</summary>
        public BoundDataGrid Grid
        {
            get { return this.uiGrid; }
        }

        /// <summary>Popup button control used to select existing records. Search engine builder registration and event handling (ItemSelected) must be implemented in parent control/page.</summary>
        public ISearchPageClient ItemPicker
        {
            get { return this.uiItemPicker; }
        }

        #endregion

        #region Public methods.

        /// <summary>Refreshes the control, ie. recreates underlying adapter and rebinds the collection.</summary>
        public void Refresh()
        {
            CreateAdapter();
            this.adapter.Reset();
            if (IsPagingUsed())
                SetPage(0, /*don't rebind*/ false);

            BindCollection();
            SwitchButtons(false);
        }

        /// <summary>Checks if there are any new of edited items that haven't submitted and accepted. May not be called before Load phase is complete.</summary>
        /// <value><b>true</b> if there are unsubmitted items.</value>
        public bool HasUnsubmittedChanges()
        {
            if (!this.isLoadComplete)
                throw new InvalidOperationException("HasUnsubmittedChanges cannot be used before Load phase has completed.");

            // In always-edit mode, all posted items are automatically accepted and then again set to edit mode.
            if (this.AlwaysEditBoundFields)
                return false;
            else
                return (this.adapter.IsEditing || this.adapter.IsInserting);
        }

        /// <summary>Rejects all new and currently edited items that haven't been accepted yet. May not be called before the Load phase is completed.</summary>
        public void RejectUnsubmittedChanges()
        {
            if (HasUnsubmittedChanges())
                RejectChanges();
        }

        /// <summary>Checks if new or edited items have been submited/accepted and whether they contain erros.</summary>
        /// <value><b>true</b> if errors are found or if the component hasn't accepted current changes; otherwise <b>false</b></value>
        public bool HasErrors()
        {
            return this.errors.HasBrokenRules;
        }

        /// <summary>
        /// Adds new business objects to collection and edits them.
        /// </summary>
        /// <param name="newItems">New objects. Ensure that all required but non-editable fields have been set to valid values before calling this method.</param>
        public void AddItems(params ISelfValidatingObject[] newItems)
        {
            if (ArrayUtil.IsNullOrEmpty(newItems))
                return;
         
            this.adapter.Insert(newItems);
        }

        /// <summary>
        /// Add new business objects to collection.
        /// </summary>
        /// <param name="acceptNewItems">Indicates whether to automatically accept all new data; or whether to remain in edit mode.</param>
        /// <param name="newItems">New objects. Ensure that all required fields have been set in case edit mode will not be used.</param>
        public void AddItems(bool acceptNewItems, params ISelfValidatingObject[] newItems)
        {
            if (ArrayUtil.IsNullOrEmpty(newItems))
                return;

            this.adapter.Insert(newItems);
            if (acceptNewItems)
                AcceptChanges();
        }

        /// <summary>Hides all butons, ie. sets their Visible property to <b>false</b>.</summary>
        public void HideAllButtons()
        {
            this.ItemPickerVisible = false;
            this.NewItemVisible = false;
            this.EditItemVisible = false;
            this.RemoveItemVisible = false;
            this.SaveCollectionVisible = false;
            this.RefetchCollectionVisible = false;
            this.OpenDetailsVisible = false;
        }

        #endregion

        #region Adapter.

        BusinessObjectsGridAdapter adapter;

        /// <summary>Ensures that the collection contains business objects and creates BusinessObjectsGridAdapter.</summary>
        private void CreateAdapter()
        {
            if (this.ConvertCollectionItemsToBusinessObject != null)
                ConvertCollectionItemsToBusinessObject(this, new CommandEventArgs(this.CommandName, this.CommandArgument));

            this.adapter = BindingsUtil.NewGridAdapter(this.uiGrid, this.Collection);
        }

        /// <summary>Ensures that BusinessObjectsGridAdapter is created.</summary>
        private void EnsureAdapter()
        {
            if (this.adapter == null)
                CreateAdapter();
        }

        /// <summary>Binds collection.</summary>
        private void BindCollection()
        {
            EnsureAdapter();
            if (this.ReadOnly)
            {
                this.adapter.DataBind();
            }
            else if (this.AlwaysEditBoundFields)
            {
                foreach (IValueObject obj in this.Collection)
                {
                    obj.IsReadOnly = false;
                    EntityModelBase classicEntity = obj as EntityModelBase;
                    if (classicEntity != null)
                        classicEntity.NullCheckEnabled = false;
                }
                this.adapter.EditAll();
            }
            else
            {
                this.adapter.DataBind();
            }
        }

        /// <summary>Ensure that the collection is bound.</summary>
        private void EnsureCollectionIsBound()
        {
            EnsureAdapter();
            if (!this.adapter.IsDataBound)
                BindCollection();
        }       

        #endregion

        #region New item.

        /// <summary>Add new item on user request.</summary>
        protected void uiNewItem_Click(object sender, EventArgs e)
        {
            if (HasErrors())
            {
                JScript.Alert(UiMessages.CorrectDisplayedErrors);
                return;
            }

            if (this.BeginInserting != null)
            {
                // Parent control/form may cancel editing.
                CancelCommandEventArgs cancelableCommand = new CancelCommandEventArgs(this.CommandName, this.CommandArgument);
                BeginInserting(this, cancelableCommand);
                if (cancelableCommand.Cancel)
                    return;
            }
    
            // New item may cause the number of pages to increase. Page that contains the new reocrd will be displayer.
            if (IsPagingUsed())
            {
                int oldPage = this.uiGrid.CurrentPageIndex;
                int newItemCount = this.Collection.Count + 1;
                int newMaxPageIndex = (newItemCount - 1) / this.uiGrid.PageSize;
                if (newMaxPageIndex > oldPage)
                    this.uiGrid.CurrentPageIndex = newMaxPageIndex;
            }

            if (this.NewItem != null)
                NewItem(this, new CommandEventArgs(this.CommandName, this.CommandArgument));

            if (adapter.IsInserting)
                SwitchButtons(/*enter edit mode*/ true);
        }

        #endregion

        #region Remove selected items.

        /// <summary>Removes selected item(s) on users request.</summary>
        protected void uiRemoveItem_Click(object sender, EventArgs e)
        {
            RemoveSelectedItems();
        }

        /// <summary>Removes selected items.</summary>
        private void RemoveSelectedItems()
        {
            if (HasErrors())
            {
                if (ShouldRejectChangesInsteadOfRemoveSelected())
                {
                    // Special case when in always-edit-all mode. Users can delete new items even if they are not valid.
                    // WARNING: currently this is allowed even if users have modified/invalidated other items.
                    // Currently there is no implemented mechanism to check if the broken rules collection contains references to other items.
                    RejectChanges();
                    // Future: collection should be re-validated and new array of broken rules collections should be displayed.
                    this.uiGrid.ClearErrors();                    
                    BindCollection();
                }
                else
                {
                    JScript.Alert(UiMessages.CorrectDisplayedErrors);
                }

                return;
            }

            if (ArrayUtil.IsNullOrEmpty(this.uiGrid.SelectedIndexes))
                return;

            // Item removing may decrease total number of required pages. Select the page that will exist after deleting.
            if (IsPagingUsed())
            {
                int oldPage = this.uiGrid.CurrentPageIndex;
                int newItemCount = this.Collection.Count - this.uiGrid.SelectedIndexes.Length;
                int newMaxPageIndex = (newItemCount - 1) / this.uiGrid.PageSize;
                if (oldPage > newMaxPageIndex)
                    this.uiGrid.CurrentPageIndex = newMaxPageIndex;
            }

            int removedItemIdx = ArrayUtil.Min<int>(this.uiGrid.SelectedIndexes);
            // Don't rebind automatically. BindCollection rebinds properly - handles indexes and edit modes.
            // Otherwise in always-edit-all mode an error would occur in the following scenario:
            //  - add new item
            //  - remove an existing item, ie. all indexes are "shifted"
            //  - exception occurs because adapter EditedItemIndexes property now contains invalid indexes ("non-shifted").
            this.adapter.RemoveSelected(/*don't rebind*/ false);
            BindCollection();

            int? idxOfItemToFocus = GetValidIndexOfItemToFocus(removedItemIdx);
            if (idxOfItemToFocus.HasValue)
                this.uiGrid.FocusItem(idxOfItemToFocus.Value);

            if (this.CollectionChanged != null)
                CollectionChanged(this, new CommandEventArgs(this.CommandName, this.CommandArgument));
        }

        private bool ShouldRejectChangesInsteadOfRemoveSelected()
        {
            // Special case when in always-edit-all mode. Users can delete new items even if they are not valid.
            // WARNING: currently this is allowed even if users have modified/invalidated other items.
            // Currently there is no implemented mechanism to check if the broken rules collection contains references to other items.
            bool shouldExecRejectChangesInsteadOfRemoveSelected = false;
            if (this.IsPostBack && !this.wasReadOnly && IsInEditAllMode())
            {
                string[] selectedValues = this.uiGrid.SelectedValues;
                bool isDeletingExactlyOneItem = (selectedValues.Length == 1);
                bool hasExactlyOneNewItem = (this.adapter.InsertedItemsIndexes != null) && (this.adapter.InsertedItemsIndexes.Length == 1);
                if (hasExactlyOneNewItem && isDeletingExactlyOneItem)
                {
                    string gridKey = selectedValues[0];
                    int adapterIndex = this.adapter.InsertedItemsIndexes[0];
                    string adapterKey = Convert.ToString(DataBinder.Eval(this.Collection[adapterIndex], this.uiGrid.DataKeyField));
                    bool isSameItem = (gridKey == adapterKey);
                    if (isSameItem)
                        shouldExecRejectChangesInsteadOfRemoveSelected = true;
                }
            }

            return shouldExecRejectChangesInsteadOfRemoveSelected;
        }

        #endregion

        #region Edit selected items.

        /// <summary>Begins editing of selected item(s) on user request.</summary>
        protected void uiEditItem_Click(object sender, EventArgs e)
        {
            if (!ArrayUtil.IsNullOrEmpty(this.uiGrid.SelectedIndexes))
            {                
                if (this.BeginEditing != null)
                {
                    // Parent control/form may cancel editing.
                    CancelCommandEventArgs cancelableCommand = new CancelCommandEventArgs(this.CommandName, this.CommandArgument);
                    BeginEditing(this, cancelableCommand);
                    if (cancelableCommand.Cancel)
                        return;
                }

                this.adapter.EditSelected();
                SwitchButtons(/*enter edit mode*/ true);
            }
        }

        #endregion
       
        #region Accept changes.

        /// <summary>Validates and accepts submited changes on user request.</summary>
        protected void uiAcceptChanges_Click(object sender, EventArgs e)
        {
            AcceptChanges();
        }

        /// <summary>Validates and accepts submited changes if all changed/new objects are valid.</summary>
        private void AcceptChanges()
        {
            int? acceptedItemIdx = GetFirstEditItemIndex();
            
            // BindCollection() manually rebinds and decides whether to EditAll.
            BusinessObjectsGridAdapter.AcceptFlags options = BusinessObjectsGridAdapter.AcceptFlags.DontDataBind;            
            this.errors = this.adapter.AcceptChanges(this.ValidationLevel, options);

            // Allow developers to optionally finish object initalization before rebinding. 
            // Eg. fetch parents, auto-compute properties...
            if (this.ConvertCollectionItemsToBusinessObject != null)
                ConvertCollectionItemsToBusinessObject(this, new CommandEventArgs(this.CommandName, this.CommandArgument));

            BindCollection();
            if (this.errors.AllRulesAreObeyed)
            {
                SwitchButtons(/*exit edit mode*/ false);
            }

            if (acceptedItemIdx.HasValue)
            {
                // Make sure the focus is "changed". In some cases, esp. when grid is lazy loaded in non-default tab, the grid item 
                // is not focused/selected if accepted by hotkey (enter). However, it's fine if user manually clicks Accept button.
                JScript.Focus(this.uiAcceptChanges);
                this.uiGrid.FocusItem(acceptedItemIdx.Value);
            }

            if (this.CollectionChanged != null)
                CollectionChanged(this, new CommandEventArgs(this.CommandName, this.CommandArgument));
        }

        #endregion

        #region Reject changes.

        /// <summary>Rejects changed data on user request.</summary>
        protected void uiRejectChanges_Click(object sender, EventArgs e)
        {
            RejectChanges();
        }

        /// <summary>Rejects new/changed data.</summary>
        private void RejectChanges()
        {
            int? rejectedItemIdx = GetFirstEditItemIndex();
            if (IsPagingUsed())
            {
                // Page count might have been reduced if new items have been rejected, ie. removed from collection.
                int[] allRejectedIndexes = this.adapter.InsertedItemsIndexes ?? new int[0];
                int newItemCount = this.Collection.Count - allRejectedIndexes.Length;
                int maxPageIdx = (newItemCount - 1) / this.uiGrid.PageSize;
                if (this.uiGrid.CurrentPageIndex > maxPageIdx)
                    this.uiGrid.CurrentPageIndex = maxPageIdx;
            }

            this.adapter.RejectChanges();
            SwitchButtons(/*exit edit mode*/ false);

            int? idxOfItemToFocus = GetValidIndexOfItemToFocus(rejectedItemIdx);
            if (idxOfItemToFocus.HasValue)
            {
                // Make sure the focus is "changed". In some cases, esp. when grid is lazy loaded in non-default tab, the grid item 
                // is not focused/selected if rejected by hotkey (enter). However, it's fine if user manually clicks Reject button.
                JScript.Focus(this.uiRejectChanges);
                this.uiGrid.FocusItem(idxOfItemToFocus.Value);
            }
        }        

        #endregion

        #region Paging.

        /// <summary>Changes current page on user request.</summary>
        protected void uiGrid_PageIndexChanged(object source, DataGridPageChangedEventArgs e)
        {
            // This handles errors that might have occured while in always-edit-all mode.
            if (HasErrors())
            {
                JScript.Alert(UiMessages.CorrectDisplayedErrors);
                return;
            }

            // This ensures that an edited item while in single-row-edit mode is accepted.
            if (IsEditingInSingleRowEditMode())
            {
                JScript.Alert(this.uiSubmitTheRecordYouAreCurrentlyEditing.Text);
                return;
            }

            SetPage(e.NewPageIndex, /*rebind*/ true);
        }

        /// <summary>Sets the specified page as current.</summary>
        private void SetPage(int pageIdx, bool rebind)
        {
            if (pageIdx == this.uiGrid.CurrentPageIndex)
                return;

            if (pageIdx < this.uiGrid.PageCount)
            {
                try { this.uiGrid.CurrentPageIndex = pageIdx; }
                catch { }
            }
            else
            {
                this.uiGrid.CurrentPageIndex = 0;
            }

            if (rebind)
                BindCollection();
        }

        #endregion

        #region Item picker/popup.

        /// <summary>Adds item that user has selected in the popup window.</summary>
        protected void uiItemPicker_ItemSelected(object sender, CommandEventArgs e)
        {
            if (HasErrors())
            {
                JScript.Alert(UiMessages.CorrectDisplayedErrors);
                return;
            }

            if (this.ItemPicked == null)
                return;

            // New item may cause the number of pages to increase. Page that contains the new reocrd will be displayed.
            if (IsPagingUsed())
            {
                int oldPage = this.uiGrid.CurrentPageIndex;
                int newItemCount = this.Collection.Count + 1;
                int newMaxPageIndex = (newItemCount - 1) / this.uiGrid.PageSize;
                if (newMaxPageIndex > oldPage)
                    this.uiGrid.CurrentPageIndex = newMaxPageIndex;
            }
            
            ItemPicked(this, e);

            if (adapter.IsInserting)
                SwitchButtons(/*enter edit mode*/ true);
        }

        #endregion

        #region SaveCollection, RefetchCollection.

        /// <summary>Persists collection on user request.</summary>
        protected void uiSaveCollection_Click(object sender, EventArgs e)
        {
            if (this.SaveCollection != null)
                SaveCollection(this, new CommandEventArgs(this.CommandName, this.CommandArgument));
        }

        /// <summary>Refetches collection on user request discarding any unsaved data.</summary>
        protected void uiRefetchCollection_Click(object sender, EventArgs e)
        {
            if (this.RefetchCollection != null)
                RefetchCollection(this, new CommandEventArgs(this.CommandName, this.CommandArgument));
        }

        #endregion        

        #region Util, checkers, indexes...

        /// <summary>Checks if ReadOnly flag has changed since last time the control was rendered, ie. since last HTTP request was processed (either first load or post-back).</summary>
        private bool HasReadOnlyChanged()
        {
            // Old value is persisted in control state.
            bool hasChanged = this.IsPostBack && (this.ReadOnly != this.wasReadOnly);
            return hasChanged;
        }

        /// <summary>Checks if control is in edit mode and only a single row is being edited.</summary>
        private bool IsEditingInSingleRowEditMode()
        {
            if (this.ReadOnly || this.AlwaysEditBoundFields)
                return false;

            bool isInEditMode = (this.adapter.IsEditing || this.adapter.IsInserting);
            return isInEditMode;
        }

        /// <summary>Checsk if control is in edit-all, i.e. spreadsheet-like, mode.</summary>
        private bool IsInEditAllMode()
        {
            return (!this.ReadOnly && this.AlwaysEditBoundFields);
        }

        /// <summary>Checks if paging is used.</summary>
        private bool IsPagingUsed()
        { 
            return (this.uiGrid.AllowPaging && this.uiGrid.PageSize > 0);
        }

        /// <summary>Gets index of first edited item. Null if no items are being edited.</summary>
        private int? GetFirstEditItemIndex()
        {
            int[] itemIndexes = ArrayUtil.Union(this.adapter.EditedItemsIndexes, this.adapter.InsertedItemsIndexes);
            return ArrayUtil.MinValue<int>(itemIndexes);
        }

        /// <summary>Checks if desired grid item index is valid. If yes then it is returned by the method; otherwise the maximum allowed item index as computed by GetMaxAllowedItemIndex method is returned.</summary>
        private int? GetValidIndexOfItemToFocus(int? desiredIndex)
        {
            int idxOfItemToFocus;
            if (desiredIndex.HasValue)
                idxOfItemToFocus = Math.Min(GetMaxAllowedItemIndex(), desiredIndex.Value);
            else
                idxOfItemToFocus = GetMaxAllowedItemIndex();

            if (idxOfItemToFocus < 0)
                return null;
            else
                return idxOfItemToFocus;
        }

        /// <summary>Gets maximum alloweed index which depends on whether the collection is empty and if paging is used.</summary>
        private int GetMaxAllowedItemIndex()
        {
            int maxAllowedIdx;
            if (ArrayUtil.IsNullOrEmpty(this.Collection))
            {
                maxAllowedIdx = -1;
            }
            else if (this.uiGrid.AllowPaging)
            {
                bool isLastPageSelected = (this.uiGrid.CurrentPageIndex == this.uiGrid.PageCount - 1);
                if (isLastPageSelected)
                    maxAllowedIdx = (this.Collection.Count % this.uiGrid.PageSize) - 1;
                else
                    maxAllowedIdx = this.uiGrid.PageSize - 1;
            }
            else
            {
                maxAllowedIdx = this.Collection.Count - 1;
            }

            return maxAllowedIdx;
        }

        #endregion

        /// <summary>Fires OpenDetails event when user requests it.</summary>
        protected void uiOpenDetails_Click(object sender, EventArgs e)
        {
            if (this.OpenDetails != null && !ArrayUtil.IsNullOrEmpty(this.uiGrid.SelectedIndexes))
                OpenDetails(this, new CommandEventArgs(this.CommandName, this.CommandArgument));
        }

        /// <summary>Gets or sets text property of the "Remove item" button.</summary>
        public string RemoveItemText
        {
            get { return this.uiRemoveItem.Text; }
            set { this.uiRemoveItem.Text = value ?? ""; }
        }

        /// <summary>Gets the selected key values. There may be more than one value if grid is in multi-row select mode.</summary>
        public string[] SelectedValues
        {
            get { return this.Grid.SelectedValues; }
        }
    }
}